@cccsaurora/howler-ui 2.17.0-dev.617 → 2.18.0-dev.626

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 (193) hide show
  1. package/api/index.d.ts +0 -2
  2. package/api/index.js +2 -4
  3. package/api/search/index.d.ts +1 -2
  4. package/api/search/index.js +1 -2
  5. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  6. package/components/app/App.js +0 -14
  7. package/components/app/providers/FavouritesProvider.js +2 -2
  8. package/components/app/providers/HitSearchProvider.d.ts +1 -0
  9. package/components/app/providers/HitSearchProvider.js +11 -6
  10. package/components/app/providers/HitSearchProvider.test.js +32 -11
  11. package/components/app/providers/LocalStorageProvider.js +1 -1
  12. package/components/app/providers/ParameterProvider.d.ts +2 -9
  13. package/components/app/providers/ParameterProvider.js +240 -165
  14. package/components/app/providers/ParameterProvider.test.js +14 -307
  15. package/components/elements/EditRow.d.ts +4 -1
  16. package/components/elements/EditRow.js +4 -4
  17. package/components/elements/PluginTypography.d.ts +1 -2
  18. package/components/elements/PluginTypography.js +2 -3
  19. package/components/elements/UserList.d.ts +2 -5
  20. package/components/elements/UserList.js +5 -14
  21. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  22. package/components/elements/display/ChipPopper.d.ts +1 -1
  23. package/components/elements/display/ChipPopper.js +1 -1
  24. package/components/elements/display/HowlerCard.js +1 -1
  25. package/components/elements/display/Modal.js +0 -1
  26. package/components/elements/display/icons/BundleButton.d.ts +6 -0
  27. package/components/elements/display/icons/BundleButton.js +32 -0
  28. package/components/elements/hit/HitBanner.js +48 -28
  29. package/components/elements/hit/HitCard.js +1 -1
  30. package/components/elements/{ObjectDetails.js → hit/HitDetails.js} +17 -17
  31. package/components/elements/hit/HitOutline.js +7 -3
  32. package/components/elements/hit/{HitPreview.d.ts → HitQuickSearch.d.ts} +3 -3
  33. package/components/elements/hit/{HitPreview.js → HitQuickSearch.js} +4 -10
  34. package/components/elements/hit/HitRelated.d.ts +1 -1
  35. package/components/elements/hit/HitRelated.js +3 -30
  36. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  37. package/components/elements/hit/related/PivotLink.js +1 -1
  38. package/components/elements/hit/related/RelatedLink.d.ts +1 -0
  39. package/components/elements/hit/related/RelatedLink.js +2 -2
  40. package/components/elements/view/ViewTitle.js +1 -1
  41. package/components/hooks/useHitActions.d.ts +1 -1
  42. package/components/hooks/useHitActions.js +2 -2
  43. package/components/hooks/useHitSelection.js +24 -3
  44. package/components/hooks/useLocalStorage.d.ts +13 -0
  45. package/components/hooks/useLocalStorage.js +53 -0
  46. package/components/hooks/useLocalStorageItem.d.ts +18 -0
  47. package/components/hooks/useLocalStorageItem.js +78 -0
  48. package/components/hooks/useLocalStorageItem.test.d.ts +1 -0
  49. package/components/hooks/useLocalStorageItem.test.js +144 -0
  50. package/components/hooks/useMyLocalStorage.js +2 -2
  51. package/components/hooks/useMyPreferences.js +1 -10
  52. package/components/hooks/useMySearch.js +2 -2
  53. package/components/hooks/useMySitemap.js +1 -4
  54. package/components/hooks/useMyTheme.js +2 -9
  55. package/components/routes/action/view/ActionSearch.js +1 -1
  56. package/components/routes/advanced/QueryBuilder.js +1 -1
  57. package/components/routes/analytics/AnalyticDetails.js +2 -2
  58. package/components/routes/analytics/AnalyticSearch.js +1 -1
  59. package/components/routes/help/ApiDocumentation.js +1 -1
  60. package/components/routes/help/BundleDocumentation.d.ts +3 -0
  61. package/components/routes/help/BundleDocumentation.js +12 -0
  62. package/components/routes/help/HitDocumentation.js +3 -1
  63. package/components/routes/help/markdown/en/bundles.md.js +1 -0
  64. package/components/routes/help/markdown/fr/bundles.md.js +1 -0
  65. package/components/routes/hits/search/BundleParentMenu.d.ts +6 -0
  66. package/components/routes/hits/search/BundleParentMenu.js +32 -0
  67. package/components/routes/hits/search/HitContextMenu.js +2 -3
  68. package/components/routes/hits/search/InformationPane.d.ts +0 -1
  69. package/components/routes/hits/search/InformationPane.js +28 -6
  70. package/components/routes/hits/search/LayoutSettings.d.ts +3 -0
  71. package/components/routes/hits/search/LayoutSettings.js +18 -0
  72. package/components/routes/hits/search/QuerySettings.js +1 -2
  73. package/components/routes/hits/search/QuerySettings.test.js +9 -14
  74. package/components/routes/hits/search/SearchPane.js +37 -13
  75. package/components/routes/hits/search/ViewLink.js +1 -1
  76. package/components/routes/hits/search/grid/EnhancedCell.js +1 -1
  77. package/components/routes/hits/view/HitViewer.js +4 -3
  78. package/components/routes/home/AnalyticCard.d.ts +2 -3
  79. package/components/routes/home/AnalyticCard.js +2 -2
  80. package/components/routes/home/ViewCard.js +1 -1
  81. package/components/routes/home/ViewRefresh.d.ts +23 -0
  82. package/components/routes/home/ViewRefresh.js +67 -0
  83. package/components/routes/home/index.js +9 -46
  84. package/components/{elements/MarkdownEditor.js → routes/overviews/OverviewEditor.js} +3 -3
  85. package/components/routes/overviews/OverviewViewer.js +2 -2
  86. package/components/routes/settings/LocalSection.js +2 -1
  87. package/locales/en/translation.json +6 -42
  88. package/locales/fr/translation.json +4 -35
  89. package/models/WithMetadata.d.ts +1 -2
  90. package/models/entities/generated/{ThreatEnrichment.d.ts → Enrichment.d.ts} +1 -1
  91. package/models/entities/generated/Howler.d.ts +4 -0
  92. package/models/entities/generated/Rule.d.ts +10 -2
  93. package/models/entities/generated/Threat.d.ts +2 -2
  94. package/package.json +3 -18
  95. package/plugins/clue/components/ClueTypography.js +2 -2
  96. package/plugins/clue/utils.d.ts +1 -2
  97. package/utils/constants.d.ts +4 -3
  98. package/utils/constants.js +1 -0
  99. package/api/search/case.d.ts +0 -4
  100. package/api/search/case.js +0 -8
  101. package/api/v2/case/index.d.ts +0 -6
  102. package/api/v2/case/index.js +0 -18
  103. package/api/v2/index.d.ts +0 -4
  104. package/api/v2/index.js +0 -6
  105. package/api/v2/search/facet.d.ts +0 -3
  106. package/api/v2/search/facet.js +0 -12
  107. package/api/v2/search/index.d.ts +0 -5
  108. package/api/v2/search/index.js +0 -24
  109. package/components/elements/ObjectDetails.d.ts +0 -6
  110. package/components/elements/case/CaseCard.d.ts +0 -8
  111. package/components/elements/case/CaseCard.js +0 -39
  112. package/components/elements/case/CasePreview.d.ts +0 -6
  113. package/components/elements/case/CasePreview.js +0 -17
  114. package/components/elements/case/StatusIcon.d.ts +0 -5
  115. package/components/elements/case/StatusIcon.js +0 -13
  116. package/components/elements/hit/elements/AnalyticLink.d.ts +0 -8
  117. package/components/elements/hit/elements/AnalyticLink.js +0 -22
  118. package/components/elements/hit/related/RelatedRecords.js +0 -63
  119. package/components/elements/observable/ObservableCard.d.ts +0 -5
  120. package/components/elements/observable/ObservableCard.js +0 -7
  121. package/components/elements/observable/ObservablePreview.d.ts +0 -6
  122. package/components/elements/observable/ObservablePreview.js +0 -12
  123. package/components/hooks/useRelatedRecords.d.ts +0 -13
  124. package/components/hooks/useRelatedRecords.js +0 -32
  125. package/components/routes/cases/CaseViewer.d.ts +0 -2
  126. package/components/routes/cases/CaseViewer.js +0 -24
  127. package/components/routes/cases/Cases.d.ts +0 -2
  128. package/components/routes/cases/Cases.js +0 -101
  129. package/components/routes/cases/constants.d.ts +0 -5
  130. package/components/routes/cases/constants.js +0 -5
  131. package/components/routes/cases/detail/AlertPanel.d.ts +0 -6
  132. package/components/routes/cases/detail/AlertPanel.js +0 -32
  133. package/components/routes/cases/detail/CaseDashboard.d.ts +0 -7
  134. package/components/routes/cases/detail/CaseDashboard.js +0 -49
  135. package/components/routes/cases/detail/CaseDetails.d.ts +0 -6
  136. package/components/routes/cases/detail/CaseDetails.js +0 -61
  137. package/components/routes/cases/detail/CaseOverview.d.ts +0 -7
  138. package/components/routes/cases/detail/CaseOverview.js +0 -43
  139. package/components/routes/cases/detail/CaseSidebar.d.ts +0 -6
  140. package/components/routes/cases/detail/CaseSidebar.js +0 -36
  141. package/components/routes/cases/detail/CaseTask.d.ts +0 -11
  142. package/components/routes/cases/detail/CaseTask.js +0 -57
  143. package/components/routes/cases/detail/ItemPage.d.ts +0 -6
  144. package/components/routes/cases/detail/ItemPage.js +0 -93
  145. package/components/routes/cases/detail/RelatedCasePanel.d.ts +0 -6
  146. package/components/routes/cases/detail/RelatedCasePanel.js +0 -31
  147. package/components/routes/cases/detail/TaskPanel.d.ts +0 -7
  148. package/components/routes/cases/detail/TaskPanel.js +0 -52
  149. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +0 -12
  150. package/components/routes/cases/detail/aggregates/CaseAggregate.js +0 -19
  151. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +0 -6
  152. package/components/routes/cases/detail/aggregates/SourceAggregate.js +0 -27
  153. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +0 -13
  154. package/components/routes/cases/detail/sidebar/CaseFolder.js +0 -134
  155. package/components/routes/cases/detail/sidebar/types.d.ts +0 -3
  156. package/components/routes/cases/detail/sidebar/utils.d.ts +0 -3
  157. package/components/routes/cases/detail/sidebar/utils.js +0 -25
  158. package/components/routes/cases/hooks/useCase.d.ts +0 -13
  159. package/components/routes/cases/hooks/useCase.js +0 -38
  160. package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
  161. package/components/routes/cases/modals/ResolveModal.js +0 -59
  162. package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
  163. package/components/routes/hits/search/shared/IndexPicker.js +0 -20
  164. package/components/routes/observables/ObservableViewer.d.ts +0 -7
  165. package/components/routes/observables/ObservableViewer.js +0 -27
  166. package/models/entities/generated/AttachmentsFile.d.ts +0 -12
  167. package/models/entities/generated/Case.d.ts +0 -28
  168. package/models/entities/generated/DestinationOriginal.d.ts +0 -19
  169. package/models/entities/generated/EmailAttachment.d.ts +0 -8
  170. package/models/entities/generated/EmailParent.d.ts +0 -19
  171. package/models/entities/generated/Enrichments.d.ts +0 -7
  172. package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
  173. package/models/entities/generated/HttpResponse.d.ts +0 -11
  174. package/models/entities/generated/Item.d.ts +0 -9
  175. package/models/entities/generated/Observable.d.ts +0 -84
  176. package/models/entities/generated/ObservableCloud.d.ts +0 -20
  177. package/models/entities/generated/ObservableDestination.d.ts +0 -23
  178. package/models/entities/generated/ObservableEmail.d.ts +0 -30
  179. package/models/entities/generated/ObservableFile.d.ts +0 -36
  180. package/models/entities/generated/ObservableHowler.d.ts +0 -44
  181. package/models/entities/generated/ObservableHttp.d.ts +0 -11
  182. package/models/entities/generated/ObservableObserver.d.ts +0 -21
  183. package/models/entities/generated/ObservableOrganization.d.ts +0 -7
  184. package/models/entities/generated/ObservableProcess.d.ts +0 -34
  185. package/models/entities/generated/ObservableSource.d.ts +0 -23
  186. package/models/entities/generated/ObservableThreat.d.ts +0 -21
  187. package/models/entities/generated/ObservableTls.d.ts +0 -12
  188. package/models/entities/generated/ObserverIngress.d.ts +0 -9
  189. package/models/entities/generated/Task.d.ts +0 -10
  190. package/utils/typeUtils.d.ts +0 -7
  191. package/utils/typeUtils.js +0 -18
  192. /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
  193. /package/components/{elements/MarkdownEditor.d.ts → routes/overviews/OverviewEditor.d.ts} +0 -0
@@ -12,11 +12,13 @@ import VSBox from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox
12
12
  import VSBoxContent from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxContent';
13
13
  import VSBoxHeader from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxHeader';
14
14
  import Phrase from '@cccsaurora/howler-ui/components/elements/addons/search/phrase/Phrase';
15
+ import BundleButton from '@cccsaurora/howler-ui/components/elements/display/icons/BundleButton';
15
16
  import SocketBadge from '@cccsaurora/howler-ui/components/elements/display/icons/SocketBadge';
16
17
  import JSONViewer from '@cccsaurora/howler-ui/components/elements/display/json/JSONViewer';
17
18
  import HitActions from '@cccsaurora/howler-ui/components/elements/hit/HitActions';
18
19
  import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
19
20
  import HitComments from '@cccsaurora/howler-ui/components/elements/hit/HitComments';
21
+ import HitDetails from '@cccsaurora/howler-ui/components/elements/hit/HitDetails';
20
22
  import HitLabels from '@cccsaurora/howler-ui/components/elements/hit/HitLabels';
21
23
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
22
24
  import HitLinks from '@cccsaurora/howler-ui/components/elements/hit/HitLinks';
@@ -25,7 +27,6 @@ import HitOverview from '@cccsaurora/howler-ui/components/elements/hit/HitOvervi
25
27
  import HitRelated from '@cccsaurora/howler-ui/components/elements/hit/HitRelated';
26
28
  import HitSummary from '@cccsaurora/howler-ui/components/elements/hit/HitSummary';
27
29
  import HitWorklog from '@cccsaurora/howler-ui/components/elements/hit/HitWorklog';
28
- import ObjectDetails from '@cccsaurora/howler-ui/components/elements/ObjectDetails';
29
30
  import useMyUserList from '@cccsaurora/howler-ui/components/hooks/useMyUserList';
30
31
  import ErrorBoundary from '@cccsaurora/howler-ui/components/routes/ErrorBoundary';
31
32
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
@@ -38,13 +39,13 @@ import { getUserList } from '@cccsaurora/howler-ui/utils/hitFunctions';
38
39
  import { validateRegex } from '@cccsaurora/howler-ui/utils/stringUtils';
39
40
  import { tryParse } from '@cccsaurora/howler-ui/utils/utils';
40
41
  import LeadRenderer from '../view/LeadRenderer';
41
- const InformationPane = ({ onClose, selected: _selected }) => {
42
+ const InformationPane = ({ onClose }) => {
42
43
  const { t, i18n } = useTranslation();
43
44
  const theme = useTheme();
44
45
  const location = useLocation();
45
46
  const { emit, isOpen } = useContext(SocketContext);
46
47
  const { getMatchingOverview, getMatchingDossiers, getMatchingAnalytic } = useMatchers();
47
- const selected = useContextSelector(ParameterContext, ctx => ctx?.selected) ?? _selected;
48
+ const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
48
49
  const pluginStore = usePluginStore();
49
50
  const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
50
51
  const [userIds, setUserIds] = useState(new Set());
@@ -93,6 +94,11 @@ const InformationPane = ({ onClose, selected: _selected }) => {
93
94
  useEffect(() => {
94
95
  getMatchingOverview(hit).then(_overview => setHasOverview(!!_overview));
95
96
  }, [getMatchingOverview, hit]);
97
+ useEffect(() => {
98
+ if (tab === 'hit_aggregate' && !hit?.howler.is_bundle) {
99
+ setTab('overview');
100
+ }
101
+ }, [hit?.howler.is_bundle, tab]);
96
102
  useEffect(() => {
97
103
  if (selected && isOpen()) {
98
104
  emit({
@@ -116,13 +122,28 @@ const InformationPane = ({ onClose, selected: _selected }) => {
116
122
  }
117
123
  // eslint-disable-next-line react-hooks/exhaustive-deps
118
124
  }, [hasOverview]);
125
+ /**
126
+ * What to show as the header? If loading a skeleton, then it depends on bundle or not. Bundles don't
127
+ * show anything while normal hits do
128
+ */
129
+ const header = useMemo(() => {
130
+ if (loading && !hit?.howler?.is_bundle) {
131
+ return _jsx(Skeleton, { variant: "rounded", height: 152 });
132
+ }
133
+ else if (!!hit && !hit.howler.is_bundle) {
134
+ return _jsx(HitBanner, { layout: HitLayout.DENSE, hit: hit });
135
+ }
136
+ else {
137
+ return null;
138
+ }
139
+ }, [hit, loading]);
119
140
  const tabContent = useMemo(() => {
120
141
  if (!tab) {
121
142
  return;
122
143
  }
123
144
  return {
124
145
  overview: () => _jsx(HitOverview, { hit: hit }),
125
- details: () => _jsx(ObjectDetails, { obj: hit }),
146
+ details: () => _jsx(HitDetails, { hit: hit }),
126
147
  hit_comments: () => _jsx(HitComments, { hit: hit, users: users }),
127
148
  hit_raw: () => _jsx(JSONViewer, { data: !loading && hit, hideSearch: true, filter: filter }),
128
149
  hit_data: () => (_jsx(JSONViewer, { data: !loading && hit?.howler?.data?.map(entry => tryParse(entry)), collapse: false, hideSearch: true, filter: filter })),
@@ -140,7 +161,8 @@ const InformationPane = ({ onClose, selected: _selected }) => {
140
161
  }[tab]?.();
141
162
  }, [dossiers, filter, hit, loading, tab, users]);
142
163
  const hasError = useMemo(() => !validateRegex(filter), [filter]);
143
- return (_jsxs(VSBox, { top: 10, sx: { height: '100%', flex: 1 }, children: [_jsxs(Stack, { direction: "column", flex: 1, sx: { overflowY: 'auto', flexGrow: 1 }, position: "relative", spacing: 1, ml: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, flexShrink: 0, pr: 2, children: [_jsx(FlexOne, {}), onClose && !location.pathname.startsWith('/bundles') && (_jsx(TuiIconButton, { size: "small", onClick: onClose, tooltip: t('hit.panel.details.exit'), children: _jsx(Clear, {}) })), _jsx(SocketBadge, { size: "small" }), analytic && (_jsx(TuiIconButton, { size: "small", tooltip: t('hit.panel.analytic.open'), disabled: !analytic || loading, route: `/analytics/${analytic.analytic_id}`, children: _jsx(QueryStats, {}) })), !!hit && (_jsx(TuiIconButton, { tooltip: t('hit.panel.open'), href: `/hits/${selected}`, disabled: !hit || loading, size: "small", target: "_blank", children: _jsx(OpenInNew, {}) }))] }), _jsx(Box, { pr: 2, children: loading || !hit ? (_jsx(Skeleton, { variant: "rounded", height: 152 })) : (_jsx(HitBanner, { layout: HitLayout.DENSE, hit: hit })) }), !!hit &&
164
+ return (_jsxs(VSBox, { top: 10, sx: { height: '100%', flex: 1 }, children: [_jsxs(Stack, { direction: "column", flex: 1, sx: { overflowY: 'auto', flexGrow: 1 }, position: "relative", spacing: 1, ml: 2, children: [_jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, flexShrink: 0, pr: 2, sx: [hit?.howler?.is_bundle && { position: 'absolute', top: 1, right: 0, zIndex: 1100 }], children: [_jsx(FlexOne, {}), onClose && !location.pathname.startsWith('/bundles') && (_jsx(TuiIconButton, { size: "small", onClick: onClose, tooltip: t('hit.panel.details.exit'), children: _jsx(Clear, {}) })), _jsx(SocketBadge, { size: "small" }), analytic && (_jsx(TuiIconButton, { size: "small", tooltip: t('hit.panel.analytic.open'), disabled: !analytic || loading, route: `/analytics/${analytic.analytic_id}`, children: _jsx(QueryStats, {}) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles, disabled: loading }), !!hit && !hit.howler.is_bundle && (_jsx(TuiIconButton, { tooltip: t('hit.panel.open'), href: `/hits/${selected}`, disabled: !hit || loading, size: "small", target: "_blank", children: _jsx(OpenInNew, {}) }))] }), _jsx(Box, { pr: 2, children: header }), !!hit &&
165
+ !hit.howler.is_bundle &&
144
166
  (!loading ? (_jsxs(_Fragment, { children: [_jsx(HitOutline, { hit: hit, layout: HitLayout.DENSE }), _jsx(HitLabels, { hit: hit })] })) : (_jsx(Skeleton, { height: 124 }))), _jsx(HitLinks, { hit: hit, analytic: analytic, dossiers: dossiers }), _jsxs(VSBoxHeader, { ml: -1, mr: -1, pb: 1, sx: { top: '0px' }, children: [_jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: {
145
167
  display: 'flex',
146
168
  flexDirection: 'row',
@@ -170,7 +192,7 @@ const InformationPane = ({ onClose, selected: _selected }) => {
170
192
  right: theme.spacing(-0.5)
171
193
  },
172
194
  '& > svg': { zIndex: 2 }
173
- }, badgeContent: hit?.howler.comment?.length ?? 0, children: _jsx(Comment, {}) }) }), value: "hit_comments", onClick: () => setTab('hit_comments') }), hasOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
195
+ }, badgeContent: hit?.howler.comment?.length ?? 0, children: _jsx(Comment, {}) }) }), value: "hit_comments", onClick: () => setTab('hit_comments') }), hit?.howler?.is_bundle && (_jsx(Tab, { label: t('hit.viewer.aggregate'), value: "hit_aggregate", onClick: () => setTab('hit_aggregate') })), hasOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
174
196
  // eslint-disable-next-line react/no-array-index-key
175
197
  , { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [lead.icon && _jsx(Icon, { icon: lead.icon }), _jsx("span", { children: i18n.language === 'en' ? lead.label.en : lead.label.fr })] }), value: 'lead:' + index, onClick: () => setTab('lead:' + index) }, 'lead:' + index))), dossiers.flatMap((_dossier, dossierIndex) => (_dossier.leads ?? []).map((_lead, leadIndex) => (_jsx(Tab
176
198
  // eslint-disable-next-line react/no-array-index-key
@@ -0,0 +1,3 @@
1
+ import type { FC } from 'react';
2
+ declare const LayoutSettings: FC;
3
+ export default LayoutSettings;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ArrowDropDown, List, Settings, TableChart, ViewComfy, ViewCompact, ViewModule } from '@mui/icons-material';
3
+ import { FormLabel, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material';
4
+ import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
5
+ import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
6
+ import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
7
+ import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { useContextSelector } from 'use-context-selector';
10
+ import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
11
+ const LayoutSettings = () => {
12
+ const { t } = useTranslation();
13
+ const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
14
+ const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
15
+ const [hitLayout, setHitLayout] = useMyLocalStorageItem(StorageKey.HIT_LAYOUT, false);
16
+ return (_jsx(ChipPopper, { icon: _jsx(Settings, {}), deleteIcon: _jsx(ArrowDropDown, {}), toggleOnDelete: true, disablePortal: false, slotProps: { chip: { size: 'medium' } }, placement: "bottom-end", children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", alignItems: "center", justifyContent: "space-between", spacing: 1, children: [_jsx(FormLabel, { children: t('page.settings.local.hits.display_type') }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", children: [_jsx(ToggleButton, { value: "list", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(List, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.list') })] }) }), _jsx(ToggleButton, { value: "grid", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(TableChart, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.grid') })] }) })] })] }), _jsxs(Stack, { direction: "row", alignItems: "center", justifyContent: "space-between", spacing: 1, children: [_jsx(FormLabel, { children: t('page.settings.local.hits.layout') }), _jsxs(ToggleButtonGroup, { exclusive: true, size: "small", value: hitLayout, onChange: (_, value) => setHitLayout(value), children: [_jsx(ToggleButton, { value: HitLayout.DENSE, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewCompact, {}), _jsx("span", { children: t('page.settings.local.hits.layout.dense') })] }) }), _jsx(ToggleButton, { value: HitLayout.NORMAL, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewModule, {}), _jsx("span", { children: t('page.settings.local.hits.layout.normal') })] }) }), _jsx(ToggleButton, { value: HitLayout.COMFY, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewComfy, {}), _jsx("span", { children: t('page.settings.local.hits.layout.comfy') })] }) })] })] })] }) }));
17
+ };
18
+ export default LayoutSettings;
@@ -9,7 +9,6 @@ import { useTranslation } from 'react-i18next';
9
9
  import { useContextSelector } from 'use-context-selector';
10
10
  import HitFilter from './shared/HitFilter';
11
11
  import HitSort from './shared/HitSort';
12
- import IndexPicker from './shared/IndexPicker';
13
12
  import SearchSpan from './shared/SearchSpan';
14
13
  import ViewLink from './ViewLink';
15
14
  const QuerySettings = ({ boxSx }) => {
@@ -26,6 +25,6 @@ const QuerySettings = ({ boxSx }) => {
26
25
  await fetchViews();
27
26
  addView('');
28
27
  };
29
- return (_jsx(Box, { sx: boxSx ?? { position: 'relative', maxWidth: '1200px' }, children: _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Grid, { container: true, spacing: 1, sx: theme => ({ ml: `${theme.spacing(-1)} !important`, mt: `${theme.spacing(-1)} !important` }), children: [_jsx(Grid, { item: true, children: _jsx(IndexPicker, {}) }), _jsx(Grid, { item: true, children: _jsx(HitSort, {}) }), _jsx(Grid, { item: true, children: _jsx(SearchSpan, {}) }), currentViews?.map((view, id) => (_jsx(Grid, { item: true, children: _jsx(ViewLink, { id: id, viewId: view }) }, view))), filters?.map((filter, id) => (_jsx(Grid, { item: true, children: _jsx(HitFilter, { id: id, value: filter }) }, filter)))] }), _jsx(ChipPopper, { icon: _jsx(Add, {}), deleteIcon: _jsx(ArrowDropDown, {}), toggleOnDelete: true, closeOnClick: true, slotProps: { chip: { size: 'small', color: 'primary' }, paper: { sx: { p: 1 } } }, children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Button, { id: "add-filter", "aria-label": t('hit.search.filter.add'), variant: "outlined", onClick: () => addFilter('howler.assessment:*'), disabled: filters?.some(filter => filter.endsWith('*')), sx: { display: 'flex', pl: 1 }, children: [_jsx(Add, { fontSize: "small", sx: { mr: 1 } }), _jsx("div", { style: { flex: 1 } }), _jsx("span", { children: t('hit.search.filter.add') })] }), _jsxs(Button, { id: "add-view", "aria-label": t('hit.search.view.add'), variant: "outlined", onClick: onAddView, disabled: !allowAddViews, sx: { display: 'flex', pl: 1 }, children: [_jsx(Add, { fontSize: "small", sx: { mr: 1 } }), _jsx("div", { style: { flex: 1 } }), _jsx("span", { children: t('hit.search.view.add') })] })] }) })] }) }));
28
+ return (_jsx(Box, { sx: boxSx ?? { position: 'relative', maxWidth: '1200px' }, children: _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Grid, { container: true, spacing: 1, sx: theme => ({ ml: `${theme.spacing(-1)} !important`, mt: `${theme.spacing(-1)} !important` }), children: [_jsx(Grid, { item: true, children: _jsx(HitSort, {}) }), _jsx(Grid, { item: true, children: _jsx(SearchSpan, {}) }), currentViews?.map((view, id) => (_jsx(Grid, { item: true, children: _jsx(ViewLink, { id: id, viewId: view }) }, view))), filters?.map((filter, id) => (_jsx(Grid, { item: true, children: _jsx(HitFilter, { id: id, value: filter }) }, filter)))] }), _jsx(ChipPopper, { icon: _jsx(Add, {}), deleteIcon: _jsx(ArrowDropDown, {}), toggleOnDelete: true, closeOnClick: true, slotProps: { chip: { size: 'small', color: 'primary' }, paper: { sx: { p: 1 } } }, children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Button, { id: "add-filter", "aria-label": t('hit.search.filter.add'), variant: "outlined", onClick: () => addFilter('howler.assessment:*'), disabled: filters?.some(filter => filter.endsWith('*')), sx: { display: 'flex', pl: 1 }, children: [_jsx(Add, { fontSize: "small", sx: { mr: 1 } }), _jsx("div", { style: { flex: 1 } }), _jsx("span", { children: t('hit.search.filter.add') })] }), _jsxs(Button, { id: "add-view", "aria-label": t('hit.search.view.add'), variant: "outlined", onClick: onAddView, disabled: !allowAddViews, sx: { display: 'flex', pl: 1 }, children: [_jsx(Add, { fontSize: "small", sx: { mr: 1 } }), _jsx("div", { style: { flex: 1 } }), _jsx("span", { children: t('hit.search.view.add') })] })] }) })] }) }));
30
29
  };
31
30
  export default memo(QuerySettings);
@@ -16,9 +16,6 @@ vi.mock('./shared/HitSort', () => ({
16
16
  vi.mock('./shared/SearchSpan', () => ({
17
17
  default: () => _jsx("div", { id: "search-span", children: "SearchSpan" })
18
18
  }));
19
- vi.mock('./shared/IndexPicker', () => ({
20
- default: () => _jsx("div", { id: "index-picker", children: "IndexPicker" })
21
- }));
22
19
  vi.mock('./ViewLink', () => ({
23
20
  default: ({ id, viewId }) => (_jsxs("div", { id: `view-link-${id}`, "data-view-id": viewId, children: ["ViewLink ", id, ": ", viewId] }))
24
21
  }));
@@ -36,7 +33,6 @@ const mockFetchViews = vi.fn();
36
33
  let mockParameterContext = {
37
34
  filters: [],
38
35
  views: [],
39
- indexes: [],
40
36
  addFilter: mockAddFilter,
41
37
  addView: mockAddView
42
38
  };
@@ -274,16 +270,16 @@ describe('QuerySettings', () => {
274
270
  mockParameterContext.views = ['view-1'];
275
271
  const { container } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
276
272
  const gridItems = container.querySelectorAll('[class*="MuiGrid-item"]');
277
- // IndexPicker + HitSort + SearchSpan + 1 view + 2 filters = 5 items
278
- expect(gridItems.length).toBe(6);
273
+ // HitSort + SearchSpan + 1 view + 2 filters = 5 items
274
+ expect(gridItems.length).toBe(5);
279
275
  });
280
276
  it('should render correct number of items with multiple views', () => {
281
277
  mockParameterContext.filters = ['filter1'];
282
278
  mockParameterContext.views = ['view-1', 'view-2', 'view-3'];
283
279
  const { container } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
284
280
  const gridItems = container.querySelectorAll('[class*="MuiGrid-item"]');
285
- // IndexPicker + HitSort + SearchSpan + 3 views + 1 filter = 6 items
286
- expect(gridItems.length).toBe(7);
281
+ // HitSort + SearchSpan + 3 views + 1 filter = 6 items
282
+ expect(gridItems.length).toBe(6);
287
283
  });
288
284
  });
289
285
  describe('Edge Cases', () => {
@@ -512,12 +508,11 @@ describe('QuerySettings', () => {
512
508
  mockParameterContext.views = ['view-1'];
513
509
  const { container } = render(_jsx(QuerySettings, {}), { wrapper: Wrapper });
514
510
  const gridItems = container.querySelectorAll('[class*="MuiGrid-item"]');
515
- // Order: IndexPicker, HitSort, SearchSpan, ViewLink(s), HitFilter(s)
516
- expect(gridItems[0]).toContainElement(screen.getByTestId('index-picker'));
517
- expect(gridItems[1]).toContainElement(screen.getByTestId('hit-sort'));
518
- expect(gridItems[2]).toContainElement(screen.getByTestId('search-span'));
519
- expect(gridItems[3]).toContainElement(screen.getByTestId('view-link-0'));
520
- expect(gridItems[4]).toContainElement(screen.getByTestId('hit-filter-0'));
511
+ // Order: HitSort, SearchSpan, ViewLink(s), HitFilter(s)
512
+ expect(gridItems[0]).toContainElement(screen.getByTestId('hit-sort'));
513
+ expect(gridItems[1]).toContainElement(screen.getByTestId('search-span'));
514
+ expect(gridItems[2]).toContainElement(screen.getByTestId('view-link-0'));
515
+ expect(gridItems[3]).toContainElement(screen.getByTestId('hit-filter-0'));
521
516
  });
522
517
  it('should pass correct props to ViewLink components', () => {
523
518
  mockParameterContext.views = ['view-1', 'view-2'];
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ErrorOutline, List, SavedSearch, TableChart, Terminal } from '@mui/icons-material';
3
- import { Box, IconButton, LinearProgress, Stack, ToggleButton, ToggleButtonGroup, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material';
2
+ import { Close, ErrorOutline, SavedSearch, Terminal } from '@mui/icons-material';
3
+ import { Box, IconButton, LinearProgress, Stack, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material';
4
4
  import { grey } from '@mui/material/colors';
5
5
  import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
6
6
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
@@ -14,22 +14,26 @@ import VSBoxContent from '@cccsaurora/howler-ui/components/elements/addons/layou
14
14
  import VSBoxHeader from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxHeader';
15
15
  import SearchPagination from '@cccsaurora/howler-ui/components/elements/addons/search/SearchPagination';
16
16
  import SearchTotal from '@cccsaurora/howler-ui/components/elements/addons/search/SearchTotal';
17
+ import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
18
+ import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
17
19
  import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
18
20
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
19
21
  import useHitSelection from '@cccsaurora/howler-ui/components/hooks/useHitSelection';
20
- import useMyLocalStorage, { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
21
- import React, { memo, useCallback, useMemo } from 'react';
22
+ import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
23
+ import React, { memo, useCallback, useEffect, useMemo } from 'react';
22
24
  import { isMobile } from 'react-device-detect';
23
25
  import { useTranslation } from 'react-i18next';
24
- import { Link } from 'react-router-dom';
26
+ import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
25
27
  import { useContextSelector } from 'use-context-selector';
26
28
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
29
+ import BundleParentMenu from './BundleParentMenu';
30
+ import { BundleScroller } from './BundleScroller';
27
31
  import HitContextMenu from './HitContextMenu';
28
32
  import HitQuery from './HitQuery';
33
+ import LayoutSettings from './LayoutSettings';
29
34
  import QuerySettings from './QuerySettings';
30
35
  const Item = memo(({ hit, onClick }) => {
31
36
  const theme = useTheme();
32
- const { get } = useMyLocalStorage();
33
37
  const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
34
38
  const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
35
39
  const checkMiddleClick = useCallback((e, id) => {
@@ -39,7 +43,8 @@ const Item = memo(({ hit, onClick }) => {
39
43
  e.preventDefault();
40
44
  }
41
45
  }, []);
42
- const layout = useMemo(() => (isMobile ? HitLayout.COMFY : (get(StorageKey.HIT_LAYOUT) ?? HitLayout.NORMAL)), [get]);
46
+ const [hitLayout] = useMyLocalStorageItem(StorageKey.HIT_LAYOUT, HitLayout.NORMAL);
47
+ const layout = useMemo(() => (isMobile ? HitLayout.COMFY : hitLayout), [hitLayout]);
43
48
  // Search result list item renderer.
44
49
  return (_jsx(Box, { id: hit.howler.id, onAuxClick: e => checkMiddleClick(e, hit.howler.id), onClick: ev => onClick(ev, hit), sx: [
45
50
  {
@@ -48,7 +53,7 @@ const Item = memo(({ hit, onClick }) => {
48
53
  '& span,p,h6': {
49
54
  cursor: 'text'
50
55
  },
51
- '& > .MuiPaper-root': {
56
+ '& .MuiPaper-root': {
52
57
  border: '4px solid transparent',
53
58
  boxShadow: `0px 0px 0px 0px transparent`,
54
59
  transition: theme.transitions.create(['border-color', 'box-shadow'])
@@ -62,10 +67,10 @@ const Item = memo(({ hit, onClick }) => {
62
67
  }
63
68
  },
64
69
  selectedHits.some(_hit => _hit.howler.id === hit.howler.id) && {
65
- '& > .MuiPaper-root': { borderColor: grey[500], boxShadow: `0px 0px 5px 2px ${grey[500]}` }
70
+ '& .MuiPaper-root': { borderColor: grey[500], boxShadow: `0px 0px 5px 2px ${grey[500]}` }
66
71
  },
67
72
  selected === hit.howler.id && {
68
- '& > .MuiPaper-root': {
73
+ '& .MuiPaper-root': {
69
74
  borderColor: 'primary.main',
70
75
  boxShadow: `0px 0px 5px 2px ${theme.palette.primary.main}`
71
76
  }
@@ -74,15 +79,21 @@ const Item = memo(({ hit, onClick }) => {
74
79
  });
75
80
  const SearchPane = () => {
76
81
  const { t } = useTranslation();
82
+ const location = useLocation();
83
+ const navigate = useNavigate();
84
+ const routeParams = useParams();
85
+ const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
86
+ const setSelected = useContextSelector(ParameterContext, ctx => ctx.setSelected);
77
87
  const query = useContextSelector(ParameterContext, ctx => ctx.query);
78
88
  const setOffset = useContextSelector(ParameterContext, ctx => ctx.setOffset);
79
- const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
80
- const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
81
89
  const triggerSearch = useContextSelector(HitSearchContext, ctx => ctx.search);
82
90
  const searching = useContextSelector(HitSearchContext, ctx => ctx.searching);
83
91
  const response = useContextSelector(HitSearchContext, ctx => ctx.response);
84
92
  const error = useContextSelector(HitSearchContext, ctx => ctx.error);
85
93
  const { onClick } = useHitSelection();
94
+ const getHit = useContextSelector(HitContext, ctx => ctx.getHit);
95
+ const clearSelectedHits = useContextSelector(HitContext, ctx => ctx.clearSelectedHits);
96
+ const bundleHit = useContextSelector(HitContext, ctx => location.pathname.startsWith('/bundles') ? ctx.hits[routeParams.id] : null);
86
97
  const searchPaneWidth = useMyLocalStorageItem(StorageKey.SEARCH_PANE_WIDTH, null)[0];
87
98
  const verticalSorters = useMediaQuery('(max-width: 1919px)') || (searchPaneWidth ?? Number.MAX_SAFE_INTEGER) < 900;
88
99
  const getSelectedId = useCallback((event) => {
@@ -93,7 +104,20 @@ const SearchPane = () => {
93
104
  }
94
105
  return selectedElement.id;
95
106
  }, []);
96
- return (_jsx(FlexPort, { id: "hitscrollbar", children: _jsx(PageCenter, { textAlign: "left", mt: 0, mb: 6, ml: 0, mr: 0, maxWidth: "1500px", children: _jsxs(VSBox, { top: 0, children: [_jsx(Stack, { ml: -1, mr: -1, sx: { '& .overflowingContentWidgets > *': { zIndex: '2000 !important' } }, spacing: 1, children: _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", children: t('hit.search.prompt') }), error && (_jsx(Tooltip, { title: `${t('route.advanced.error')}: ${error}`, children: _jsx(ErrorOutline, { fontSize: "small", color: "error" }) })), _jsx(FlexOne, {}), _jsx(Tooltip, { title: t('route.views.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/views/create?query=${query}`, children: _jsx(SavedSearch, {}) }) }), _jsx(Tooltip, { title: t('route.actions.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/action/execute?query=${query}`, children: _jsx(Terminal, {}) }) }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", children: [_jsx(ToggleButton, { value: "list", children: _jsx(List, {}) }), _jsx(ToggleButton, { value: "grid", children: _jsx(TableChart, {}) })] })] }) }), _jsxs(VSBoxHeader, { ml: -3, mr: -3, px: 2, pb: 1, sx: { zIndex: 989 }, children: [_jsxs(Stack, { sx: { pt: 1 }, children: [_jsxs(Stack, { sx: { position: 'relative', flex: 1 }, children: [_jsx(HitQuery, { searching: searching, triggerSearch: triggerSearch }), searching && (_jsx(LinearProgress, { sx: theme => ({
107
+ useEffect(() => {
108
+ if (location.pathname.startsWith('/bundles')) {
109
+ getHit(routeParams.id);
110
+ }
111
+ // eslint-disable-next-line react-hooks/exhaustive-deps
112
+ }, [location.pathname, routeParams.id]);
113
+ return (_jsx(FlexPort, { id: "hitscrollbar", children: _jsx(PageCenter, { textAlign: "left", mt: 0, mb: 6, ml: 0, mr: 0, maxWidth: "1500px", children: _jsxs(VSBox, { top: 0, children: [_jsxs(Stack, { ml: -1, mr: -1, sx: { '& .overflowingContentWidgets > *': { zIndex: '2000 !important' } }, spacing: 1, children: [bundleHit && (_jsx(BundleScroller, { children: _jsx(HitContextMenu, { getSelectedId: () => bundleHit.howler.id, children: _jsx(Stack, { spacing: 1, sx: { mx: -1 }, children: _jsx(HowlerCard, { sx: [
114
+ { p: 1, border: '4px solid transparent', cursor: 'pointer' },
115
+ location.pathname.startsWith('/bundles') &&
116
+ selected === routeParams.id && { borderColor: 'primary.main' }
117
+ ], onClick: () => {
118
+ clearSelectedHits(bundleHit.howler.id);
119
+ setSelected(bundleHit.howler.id);
120
+ }, children: _jsx(HitBanner, { hit: bundleHit, layout: HitLayout.DENSE, useListener: true }) }) }) }) })), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", children: t('hit.search.prompt') }), error && (_jsx(Tooltip, { title: `${t('route.advanced.error')}: ${error}`, children: _jsx(ErrorOutline, { fontSize: "small", color: "error" }) })), _jsx(FlexOne, {}), bundleHit?.howler.bundles.length > 0 && _jsx(BundleParentMenu, { bundle: bundleHit }), bundleHit && (_jsx(Tooltip, { title: t('hit.bundle.close'), children: _jsx(IconButton, { size: "small", onClick: () => navigate('/search'), children: _jsx(Close, {}) }) })), _jsx(Tooltip, { title: t('route.views.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/views/create?query=${query}`, children: _jsx(SavedSearch, {}) }) }), _jsx(Tooltip, { title: t('route.actions.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/action/execute?query=${query}`, children: _jsx(Terminal, {}) }) }), _jsx(LayoutSettings, {})] })] }), _jsxs(VSBoxHeader, { ml: -3, mr: -3, px: 2, pb: 1, sx: { zIndex: 989 }, children: [_jsxs(Stack, { sx: { pt: 1 }, children: [_jsxs(Stack, { sx: { position: 'relative', flex: 1 }, children: [_jsx(HitQuery, { searching: searching, triggerSearch: triggerSearch }), searching && (_jsx(LinearProgress, { sx: theme => ({
97
121
  position: 'absolute',
98
122
  left: 0,
99
123
  right: 0,
@@ -47,7 +47,7 @@ const ViewLink = ({ id, viewId }) => {
47
47
  }, [query, sort, span, view]);
48
48
  const options = useMemo(() => Object.values(views).filter(_view => !!_view && !currentViews?.includes(_view.view_id)), [currentViews, views]);
49
49
  if (loading) {
50
- return _jsx(Chip, { icon: _jsx(CircularProgress, { size: 12 }) });
50
+ return _jsx(Chip, { size: "small", icon: _jsx(CircularProgress, { size: 12 }) });
51
51
  }
52
52
  if (viewId === '') {
53
53
  return (_jsx(ChipPopper, { icon: _jsx(SelectAll, {}), label: t('hit.search.view.select'), deleteIcon: _jsx(ArrowDropDown, {}), toggleOnDelete: true, slotProps: { chip: { size: 'small', color: 'warning' } }, children: _jsxs(Stack, { spacing: 1, direction: "row", children: [_jsx(Autocomplete, { fullWidth: true, size: "small", options: options, getOptionLabel: _view => t(_view.title), renderOption: ({ key, ...props }, o) => (_createElement("li", { ...props, key: key },
@@ -13,6 +13,6 @@ const EnhancedCell = ({ hit, value: rawValue, sx = {}, className, field }) => {
13
13
  return (_jsx(TableCell, { sx: { borderBottom: 'none', borderRight: 'thin solid', borderRightColor: 'divider', fontSize: '0.8rem' }, children: _jsx(Stack, { direction: "row", className: className, spacing: 0.5, sx: [
14
14
  { display: 'flex', justifyContent: 'start', width: '100%', overflow: 'hidden' },
15
15
  ...(Array.isArray(sx) ? sx : [sx])
16
- ], children: values.map((value, index) => (_jsx(PluginTypography, { context: "table", sx: { fontSize: 'inherit', textOverflow: 'ellipsis' }, value: value, field: field, obj: hit, children: value }, value + index))) }) }));
16
+ ], children: values.map((value, index) => (_jsx(PluginTypography, { context: "table", sx: { fontSize: 'inherit', textOverflow: 'ellipsis' }, value: value, field: field, hit: hit, children: value }, value + index))) }) }));
17
17
  };
18
18
  export default memo(EnhancedCell);
@@ -7,11 +7,13 @@ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers'
7
7
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
8
8
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
9
9
  import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
10
+ import BundleButton from '@cccsaurora/howler-ui/components/elements/display/icons/BundleButton';
10
11
  import SocketBadge from '@cccsaurora/howler-ui/components/elements/display/icons/SocketBadge';
11
12
  import JSONViewer from '@cccsaurora/howler-ui/components/elements/display/json/JSONViewer';
12
13
  import HitActions from '@cccsaurora/howler-ui/components/elements/hit/HitActions';
13
14
  import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
14
15
  import HitComments from '@cccsaurora/howler-ui/components/elements/hit/HitComments';
16
+ import HitDetails from '@cccsaurora/howler-ui/components/elements/hit/HitDetails';
15
17
  import HitLabels from '@cccsaurora/howler-ui/components/elements/hit/HitLabels';
16
18
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
17
19
  import HitLinks from '@cccsaurora/howler-ui/components/elements/hit/HitLinks';
@@ -19,7 +21,6 @@ import HitOutline from '@cccsaurora/howler-ui/components/elements/hit/HitOutline
19
21
  import HitOverview from '@cccsaurora/howler-ui/components/elements/hit/HitOverview';
20
22
  import HitRelated from '@cccsaurora/howler-ui/components/elements/hit/HitRelated';
21
23
  import HitWorklog from '@cccsaurora/howler-ui/components/elements/hit/HitWorklog';
22
- import ObjectDetails from '@cccsaurora/howler-ui/components/elements/ObjectDetails';
23
24
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
24
25
  import useMyUserList from '@cccsaurora/howler-ui/components/hooks/useMyUserList';
25
26
  import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -93,7 +94,7 @@ const HitViewer = () => {
93
94
  }
94
95
  return {
95
96
  overview: () => _jsx(HitOverview, { hit: hit }),
96
- details: () => _jsx(ObjectDetails, { obj: hit }),
97
+ details: () => _jsx(HitDetails, { hit: hit }),
97
98
  hit_comments: () => _jsx(HitComments, { hit: hit, users: users }),
98
99
  hit_raw: () => _jsx(JSONViewer, { data: hit }),
99
100
  hit_data: () => _jsx(JSONViewer, { data: hit?.howler?.data?.map(entry => tryParse(entry)), collapse: false }),
@@ -131,7 +132,7 @@ const HitViewer = () => {
131
132
  position: 'absolute',
132
133
  top: theme.spacing(2),
133
134
  right: theme.spacing(-6)
134
- }, children: [_jsx(Tooltip, { title: t('hit.panel.view.layout'), children: _jsx(IconButton, { onClick: onOrientationChange, children: _jsx(ViewAgenda, { sx: { transition: 'rotate 250ms', rotate: orientation === 'vertical' ? '90deg' : '0deg' } }) }) }), _jsx(SocketBadge, { size: "medium" }), analytic && (_jsx(Tooltip, { title: t('hit.panel.analytic.open'), children: _jsx(IconButton, { onClick: () => navigate(`/analytics/${analytic.analytic_id}`), children: _jsx(QueryStats, {}) }) }))] }))] }), _jsx(HowlerCard, { sx: [orientation === 'horizontal' && { height: '0px' }], children: _jsx(CardContent, { sx: { padding: 1, position: 'relative' }, children: _jsx(HitActions, { hit: hit, orientation: "vertical" }) }) }), _jsx(Box, { sx: { gridColumn: '1 / span 2', mb: 1 }, children: _jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: { display: 'flex', flexDirection: 'row', pr: 2, alignItems: 'center' }, children: [hasOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
135
+ }, children: [_jsx(Tooltip, { title: t('hit.panel.view.layout'), children: _jsx(IconButton, { onClick: onOrientationChange, children: _jsx(ViewAgenda, { sx: { transition: 'rotate 250ms', rotate: orientation === 'vertical' ? '90deg' : '0deg' } }) }) }), _jsx(SocketBadge, { size: "medium" }), analytic && (_jsx(Tooltip, { title: t('hit.panel.analytic.open'), children: _jsx(IconButton, { onClick: () => navigate(`/analytics/${analytic.analytic_id}`), children: _jsx(QueryStats, {}) }) })), hit?.howler.bundles?.length > 0 && _jsx(BundleButton, { ids: hit.howler.bundles })] }))] }), _jsx(HowlerCard, { sx: [orientation === 'horizontal' && { height: '0px' }], children: _jsx(CardContent, { sx: { padding: 1, position: 'relative' }, children: _jsx(HitActions, { hit: hit, orientation: "vertical" }) }) }), _jsx(Box, { sx: { gridColumn: '1 / span 2', mb: 1 }, children: _jsxs(Tabs, { value: tab === 'overview' && !hasOverview ? 'details' : tab, sx: { display: 'flex', flexDirection: 'row', pr: 2, alignItems: 'center' }, children: [hit?.howler?.is_bundle && (_jsx(Tab, { label: t('hit.viewer.aggregate'), value: "hit_aggregate", onClick: () => setTab('hit_aggregate') })), hasOverview && (_jsx(Tab, { label: t('hit.viewer.overview'), value: "overview", onClick: () => setTab('overview') })), _jsx(Tab, { label: t('hit.viewer.details'), value: "details", onClick: () => setTab('details') }), hit?.howler.dossier?.map((lead, index) => (_jsx(Tab
135
136
  // eslint-disable-next-line react/no-array-index-key
136
137
  , { label: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [lead.icon && _jsx(Icon, { icon: lead.icon }), _jsx("span", { children: i18n.language === 'en' ? lead.label.en : lead.label.fr })] }), value: 'lead:' + index, onClick: () => setTab('lead:' + index) }, 'lead:' + index))), dossiers.flatMap((_dossier, dossierIndex) => (_dossier.leads ?? []).map((_lead, leadIndex) => (_jsx(Tab
137
138
  // eslint-disable-next-line react/no-array-index-key
@@ -1,7 +1,6 @@
1
- import type { FC } from 'react';
2
1
  export interface AnalyticSettings {
3
2
  analyticId: string;
4
3
  type: 'assessment' | 'created' | 'escalation';
5
4
  }
6
- declare const AnalyticCard: FC<AnalyticSettings>;
7
- export default AnalyticCard;
5
+ declare const _default: import("react").NamedExoticComponent<AnalyticSettings>;
6
+ export default _default;
@@ -3,7 +3,7 @@ import { CenterFocusWeak, OpenInNew } from '@mui/icons-material';
3
3
  import { Box, Card, CardContent, IconButton, Skeleton, Stack, Tooltip, Typography } from '@mui/material';
4
4
  import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
5
5
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
6
- import { useContext, useEffect, useRef, useState } from 'react';
6
+ import { memo, useContext, useEffect, useRef, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Link } from 'react-router-dom';
9
9
  import Assessment from '../analytics/widgets/Assessment';
@@ -27,4 +27,4 @@ const AnalyticCard = ({ analyticId, type }) => {
27
27
  escalation: () => (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center' }, children: _jsx(Escalation, { analytic: analytic, maxWidth: "80%" }) }))
28
28
  }[type]()] }) }));
29
29
  };
30
- export default AnalyticCard;
30
+ export default memo(AnalyticCard);
@@ -150,6 +150,6 @@ const ViewCard = ({ viewId, limit, refreshTick, onRefreshComplete }) => {
150
150
  }
151
151
  return selectedElement.id;
152
152
  }, []);
153
- return (_jsx(Card, { variant: "outlined", sx: { height: '100%' }, children: _jsxs(Stack, { spacing: 1, sx: { p: 1, minHeight: 100 }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", children: t(view?.title) || _jsx(Skeleton, { variant: "text", height: "2em", width: "100px" }) }), _jsx(IconButton, { size: "small", onClick: () => onClick(view.query), children: _jsx(OpenInNew, { fontSize: "small" }) })] }), loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { height: 150, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 160, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 140, width: "100%", variant: "rounded" })] })) : hits.length > 0 ? (_jsx(HitContextMenu, { getSelectedId: getSelectedId, children: hits.map(h => (_jsx(Card, { id: h.howler.id, variant: "outlined", sx: { cursor: 'pointer' }, onClick: () => navigate(`/hits/${h.howler.id}`), children: _jsx(CardContent, { children: _jsx(HitBanner, { layout: HitLayout.DENSE, hit: h }) }) }, h.howler.id))) })) : (_jsx(AppListEmpty, {}))] }) }));
153
+ return (_jsx(Card, { variant: "outlined", sx: { height: '100%' }, children: _jsxs(Stack, { spacing: 1, sx: { p: 1, minHeight: 100 }, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h6", children: t(view?.title) || _jsx(Skeleton, { variant: "text", height: "2em", width: "100px" }) }), _jsx(IconButton, { size: "small", onClick: () => onClick(view.query), children: _jsx(OpenInNew, { fontSize: "small" }) })] }), loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { height: 150, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 160, width: "100%", variant: "rounded" }), _jsx(Skeleton, { height: 140, width: "100%", variant: "rounded" })] })) : hits.length > 0 ? (_jsx(HitContextMenu, { getSelectedId: getSelectedId, children: hits.map(h => (_jsx(Card, { id: h.howler.id, variant: "outlined", sx: { cursor: 'pointer' }, onClick: () => navigate((h.howler.is_bundle ? '/bundles/' : '/hits/') + h.howler.id), children: _jsx(CardContent, { children: _jsx(HitBanner, { layout: HitLayout.DENSE, hit: h }) }) }, h.howler.id))) })) : (_jsx(AppListEmpty, {}))] }) }));
154
154
  };
155
155
  export default ViewCard;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Imperative handle exposed to the parent via ref.
3
+ * The parent calls `handleRefreshComplete` once each ViewCard finishes its data fetch,
4
+ * allowing ViewRefresh to track how many cards are still in flight.
5
+ */
6
+ export interface ViewRefreshHandle {
7
+ handleRefreshComplete: () => void;
8
+ }
9
+ interface ViewRefreshProps {
10
+ /** Auto-refresh interval in seconds (e.g. 15, 30, 60, 300). */
11
+ refreshRate: number;
12
+ /** Number of ViewCards currently on the dashboard. Used to track pending fetches. */
13
+ viewCardCount: number;
14
+ /** Called when a refresh cycle begins. Should update `refreshTick` in the parent to signal ViewCards. */
15
+ onRefresh: () => void;
16
+ }
17
+ /**
18
+ * Self-contained refresh control that owns the countdown timer and refreshing state.
19
+ * Isolating this state here prevents the progress ticker (which fires every `refreshRate * 10`ms)
20
+ * from causing unnecessary re-renders in the parent Home component.
21
+ */
22
+ declare const ViewRefresh: import("react").ForwardRefExoticComponent<ViewRefreshProps & import("react").RefAttributes<ViewRefreshHandle>>;
23
+ export default ViewRefresh;
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Refresh } from '@mui/icons-material';
3
+ import { Box, CircularProgress, IconButton, Tooltip } from '@mui/material';
4
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ /**
7
+ * Self-contained refresh control that owns the countdown timer and refreshing state.
8
+ * Isolating this state here prevents the progress ticker (which fires every `refreshRate * 10`ms)
9
+ * from causing unnecessary re-renders in the parent Home component.
10
+ */
11
+ const ViewRefresh = forwardRef(({ refreshRate, viewCardCount, onRefresh }, ref) => {
12
+ const { t } = useTranslation();
13
+ const [progress, setProgress] = useState(0);
14
+ const [isRefreshing, setIsRefreshing] = useState(false);
15
+ const pendingRefreshes = useRef(0);
16
+ const timerRef = useRef(null);
17
+ /**
18
+ * Called by the parent (via ref) each time a ViewCard finishes fetching.
19
+ * Clears the refreshing state once all cards have reported back.
20
+ */
21
+ const handleRefreshComplete = useCallback(() => {
22
+ pendingRefreshes.current -= 1;
23
+ if (pendingRefreshes.current <= 0) {
24
+ setIsRefreshing(false);
25
+ setProgress(0);
26
+ }
27
+ }, []);
28
+ // Expose handleRefreshComplete to the parent without leaking the rest of the component's state.
29
+ useImperativeHandle(ref, () => ({ handleRefreshComplete }), [handleRefreshComplete]);
30
+ const triggerRefresh = useCallback(() => {
31
+ setIsRefreshing(true);
32
+ pendingRefreshes.current = viewCardCount;
33
+ if (viewCardCount === 0) {
34
+ setIsRefreshing(false);
35
+ setProgress(0);
36
+ return;
37
+ }
38
+ onRefresh();
39
+ }, [viewCardCount, onRefresh]);
40
+ useEffect(() => {
41
+ if (isRefreshing)
42
+ return;
43
+ if (progress >= 100) {
44
+ triggerRefresh();
45
+ return;
46
+ }
47
+ timerRef.current = setTimeout(() => {
48
+ setProgress(prev => prev + 1);
49
+ }, refreshRate * 10);
50
+ return () => {
51
+ if (timerRef.current)
52
+ clearTimeout(timerRef.current);
53
+ };
54
+ }, [progress, isRefreshing, refreshRate, triggerRefresh]);
55
+ return (_jsxs(Box, { sx: { position: 'relative', display: 'inline-flex' }, children: [isRefreshing ? (_jsx(CircularProgress, { variant: "indeterminate" })) : (_jsx(CircularProgress, { variant: "determinate", value: progress })), _jsx(Box, { sx: {
56
+ top: 0,
57
+ left: 0,
58
+ bottom: 0,
59
+ right: 0,
60
+ position: 'absolute',
61
+ display: 'flex',
62
+ alignItems: 'center',
63
+ justifyContent: 'center'
64
+ }, children: _jsx(Tooltip, { title: t('refresh'), children: _jsx(IconButton, { onClick: triggerRefresh, disabled: isRefreshing, color: "primary", children: _jsx(Refresh, {}) }) }) })] }));
65
+ });
66
+ ViewRefresh.displayName = 'ViewRefresh';
67
+ export default ViewRefresh;